package com.epam.training.myspringioc.factory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.epam.training.myspringioc.BeanDefinition;
import com.epam.training.myspringioc.parsers.Parser;

/**
 * Create bean and put them into container
 * 
 * @author bogdan
 *
 */
public class XmlBeanFactory implements BeanFactory {
	private Map<String, Object> container;
	private Map<String, BeanDefinition> beanDefinition;

	/**
	 * Main construct
	 * 
	 * @param definition
	 * 
	 */
	public XmlBeanFactory(Map<String, BeanDefinition> definition) {
		container = new HashMap<String, Object>();
		beanDefinition = definition;
		this.fillContainer(definition);
	}

	@Override
	public Object getBean(String string) {
		return container.get(string);
	}

	@Override
	public <T> T getBean(String string, Class<T> type) {
		return type.cast(container.get(string));
	}

	/**
	 * Fill the container with beans
	 * 
	 * @param definition
	 */
	private void fillContainer(Map<String, BeanDefinition> definition) {
		String[] classId = definition.keySet().toArray(
				new String[definition.size()]);

		BeanDefinition[] beanDef = definition.values().toArray(
				new BeanDefinition[definition.size()]);

		Object bean = null;

		for (int i = 0; i < definition.size(); i++) {
			bean = getBeanFromDefinition(beanDef[i]);
			if (bean != null) {
				container.put(classId[i], bean);
			}
		}
	}

	/**
	 * Get beans definition from object model which generate from xml file
	 * 
	 * @param beanDef
	 * @return
	 */
	private Object getBeanFromDefinition(BeanDefinition beanDef) {
		try {
			Object bean;
			Class<?> clazz = Class.forName(beanDef.getClassId());
			if (beanDef.getConstructorArgs().size() < 1) {
				bean = clazz.newInstance();
			} else {
				Object[] args = constructorArg(beanDef.getConstructorArgs());
				Constructor<?> constr = clazz
						.getConstructor(constructorArgClasses(beanDef
								.getConstructorArgs()));

				bean = constr.newInstance(args);
			}

			if (beanDef.getProperties().size() == 0) {
				return bean;
			} else {
				setFieldProperty(bean, beanDef.getProperties());
				return bean;
			}
		} catch (ClassNotFoundException | InstantiationException
				| IllegalAccessException | NoSuchMethodException
				| SecurityException | IllegalArgumentException
				| InvocationTargetException e) {
			Logger.getLogger(XmlBeanFactory.class.getName()).log(Level.SEVERE,
					null, e);
			return null;
		}
	}

	/**
	 * 
	 * @param arg
	 * @return
	 * @throws ClassNotFoundException
	 */
	private Object[] constructorArg(Map<String, String> arg)
			throws ClassNotFoundException {
		List<Object> list = new ArrayList<Object>();

		String[] key = arg.keySet().toArray(new String[arg.size()]);

		for (int i = 0; i < key.length; i++) {
			if (key[i].equals(Parser.VALUE_ATRIBUTE_NAME)) {
				list.add(arg.get(key[i]));
			} else if (key[i].equals(Parser.REF_ATRIBUTE_NAME)) {
				String name = arg.get(key[i]);
				if (container.containsKey(name)) {
					list.add(container.get(name));
				}
			}
		}

		return list.toArray();
	}

	/**
	 * 
	 * @param bean
	 * @param properties
	 */
	private void setFieldProperty(final Object bean,
			final Map<String, String[]> properties) {
		final int size = properties.size();

		final String[] key = properties.keySet().toArray(new String[size]);

		for (int i = 0; i < size; i++) {
			String[] property = properties.get(key[i]);
			Object val = property[1];
			try {
				Field field = bean.getClass().getDeclaredField(key[i]);
				field.setAccessible(true);

				if (property[0].equals(Parser.REF_ATRIBUTE_NAME)) {
					Class<?> ref = Class.forName(beanDefinition
							.get(property[1]).getClassId());
					val = getBean(property[1], ref);
				}

				field.set(bean, val);
				field.setAccessible(false);
			} catch (NoSuchFieldException | SecurityException
					| ClassNotFoundException | IllegalArgumentException
					| IllegalAccessException e) {
				Logger.getLogger(XmlBeanFactory.class.getName()).log(
						Level.SEVERE, null, e);

				return;
			}
		}
	}

	/**
	 * 
	 * @param arg
	 * @return
	 * @throws ClassNotFoundException
	 */
	private Class<?>[] constructorArgClasses(Map<String, String> arg)
			throws ClassNotFoundException {
		List<Class<?>> list = new ArrayList<Class<?>>();
		String[] key = arg.keySet().toArray(new String[arg.size()]);
		String[] values = arg.values().toArray(new String[arg.size()]);

		for (int i = 0; i < key.length; i++) {
			if (key[i].equals(Parser.VALUE_ATRIBUTE_NAME)) {
				list.add(String.class);
			} else if (key[i].equals(Parser.REF_ATRIBUTE_NAME)) {
				Class<?> ref = Class.forName(beanDefinition.get(values[i])
						.getClassId());
				list.add(ref);
			}
		}

		return list.toArray(new Class<?>[list.size()]);
	}
}
